﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Sora.Data;
using Sora.HelpSystem.Package.Pages;
using System.Windows.Documents;
using Sora.HelpSystem.Utils;

namespace Sora.HelpSystem
{
    public class SearchProvider
    {
        /*
         * This is a basic search facility. Each page contains
         * the full set of embedded words, but no information is kept
         * about where the specified word is located on the page

         * Search Index format :
         * 
         * search/pages
         * 0 --> complete uri of first page
         * 1 --> complete uri of second page
         * ...
         * 
         * search/words/[search word]/[index of pages]
         * */

        public HelpDocumentProvider DocumentProvider { get; private set; }

        private IEntry Pages { get; set; }
        private IEntry Words { get; set; }

        private List<string> IndexCache { get; set; }

        private SearchCache SearchCache { get; set; }

        public SearchProvider(HelpDocumentProvider documentProvider)
        {
            if (documentProvider == null)
                throw new ArgumentNullException("documentProvider");

            SearchCache = new Utils.SearchCache(16);

            DocumentProvider = documentProvider;

            Pages = DocumentProvider.Document.InternalContent.Child("search/pages");
            Words = DocumentProvider.Document.InternalContent.Child("search/words");
        }

        public IEnumerable<HelpPage> Search(string query)
        {
            if (String.IsNullOrEmpty(query))
                yield break;
            else
            {
                string[] parts = query.Split(new char[] { '+' }, StringSplitOptions.RemoveEmptyEntries);
                
                if (parts == null || parts.Length < 1)
                    yield break;
                else
                {
                    var result = SearchExecute(parts);

                    foreach (var item in result)
                        yield return DocumentProvider.ResolveUri(item);
                }
            }
        }

        private IEnumerable<Uri> SearchExecute(IEnumerable<string> parts, bool allowIncompleteWordsSearch = true)
        {
            parts = parts.Distinct(StringComparer.OrdinalIgnoreCase);

            if (allowIncompleteWordsSearch)
            {
                /* Execute a search even for incomplete words */
                if (IndexCache == null)
                {
                    IndexCache = new List<string>();

                    foreach (var child in Words.GetChildNames())
                        IndexCache.Add(child.ToLowerInvariant());
                }

                /* From the query we builds up a list of complete words to search
                 * */
                var fullWords = from word in parts
                                select (from index_entry in IndexCache
                                        where (index_entry.IndexOf(word.ToLowerInvariant()) >= 0)
                                        select index_entry);

                /* The search here is different.
                 * Whereas in the normal mode we must match
                 * every single word in a query, here there
                 * is a logical OR in the different words found from
                 * one incomplete word
                 */

                var primary_result = new List<IEnumerable<string>>();

                foreach (var words in fullWords)
                {
                    primary_result.Add(SearchUnion(words));
                }

                return SearchIntersect(primary_result);
            }
            else
            {
                /* Execute a search for an exact word in the document 
                 * It's a direct search in the index for each words
                 */
                return SearchIntersect(from word in parts
                                       select SearchWord(word));
            }
        }

        private IEnumerable<Uri> SearchIntersect(IEnumerable<IEnumerable<string>> primary_result)
        {
            IEnumerable<Uri> result = new List<Uri>();

            bool first = true;

            foreach (var searchResult in primary_result)
            {
                if (first)
                {
                    ((List<Uri>)result).AddRange(SearchIntersectQuery(searchResult));
                    first = false;
                }
                else
                {
                    if (result.Count() <= 0)
                        break;
                    else
                        result = result.Intersect(SearchIntersectQuery(searchResult));
                }
            }

            return result;
        }

        private IEnumerable<Uri> SearchIntersectQuery(IEnumerable<string> searchResult)
        {
            return from page in searchResult.Distinct() where !String.IsNullOrEmpty(page) select new Uri(Pages[page].Value);
        }

        private IEnumerable<string> SearchWord(string word)
        {
            if (String.IsNullOrEmpty(word))
                return new string[0];
            else
            {
                SearchCacheItem searchCacheItem = null;

                string li_word = word.ToLowerInvariant();

                if (SearchCache.Get(li_word, out searchCacheItem))
                    return searchCacheItem.SearchResult;
                else
                {
                    IEntry child;

                    searchCacheItem.key = li_word;
                    searchCacheItem.valid = true;
                    return searchCacheItem.SearchResult = Words.TryGetChild(word, out child) ? child.GetFieldNames() : new string[0];
                }
            }
        }

        private IEnumerable<string> SearchUnion(IEnumerable<string> words)
        {
            var result = new List<string>();
            
            var pages_cache = new Dictionary<IField, KeyValuePair<string, string>>();

            foreach (var page_field in Pages.GetFields())
                pages_cache[page_field] = new KeyValuePair<string, string>(page_field.Name, page_field.Value);

            foreach (var word in words)
            {
                result.AddRange(SearchWord(word));
            }

            return (from string_result in result select string_result).Distinct();
        }

        public void GenerateSearchIndex(int minimumWordSize = 3)
        {
            IndexCache = null;

            Pages.ClearAll();
            Words.ClearAll();
            SearchCache.Reset();

            var work = new Queue<HelpPage>();

            work.Enqueue(DocumentProvider.Document.HelpContent.Root);

            for (int currentIndex = 0; work.Count > 0; currentIndex++)
            {
                var current = work.Dequeue();

                foreach (var child in current.Children)
                    work.Enqueue(child);

                GenerateSearchIndex(current, currentIndex, minimumWordSize);
            }
        }

        private void GenerateSearchIndex(HelpPage current, int currentIndex, int minimumWordSize)
        {
            if (current.IsSummaryVisible)
            {
                Pages[currentIndex.ToString()].Value = current.Uri.AbsoluteUri;

                var page_index = Words.Child(currentIndex.ToString());

                var doc = current.FlowContent.Source;

                TextRange range = new TextRange(doc.ContentStart, doc.ContentEnd);
                var docText = range.Text;

                /* we are avoiding a string[] for each word of the document ... */
                StringBuilder currentWord = new StringBuilder(16);

                for (int i = 0; i < docText.Length; i++)
                {
                    var c = docText[i];

                    if (IsSeparator(c))
                    {
                        ProcessSearchWord(currentWord.ToString(), current, page_index, minimumWordSize);
                        currentWord.Length = 0;
                    }
                    else
                        currentWord.Append(c);
                }

                ProcessSearchWord(currentWord.ToString(), current, page_index, minimumWordSize);

                /* we are also generating the index for the page title */
                if (!String.IsNullOrEmpty(current.PageTitle))
                {
                    string[] title_parts = current.PageTitle.Split(new char[] { ' ', '\'', '\"', '(', '[', ']', ')', ',' }, StringSplitOptions.RemoveEmptyEntries);

                    foreach (var title_part in title_parts)
                        ProcessSearchWord(title_part, current, page_index, minimumWordSize);
                }
            }
        }

        private static bool IsSeparator(char c)
        {
            return Char.IsWhiteSpace(c)
                || c == ','
                || c == '('
                || c == ')'
                || c == '['
                || c == ']'
                || c == '\''
                || c == '\"';
        }

        private void ProcessSearchWord(string currentWord, HelpPage current, IEntry page_index, int minimumWordSize)
        {
            string word = currentWord
                .Replace("/", "")
                .Replace("\\", "")
                .Replace("?", "")
                .Replace("*", "")
                .Replace(":", "")
                .Replace("\n","")
                .Replace("\t","")
                .Replace("\r","");

            if (word.Length < minimumWordSize)
                return;
            else
            {
                var item = Words.Child(currentWord)[page_index.Name];
            }
        }
    }
}
